///PHILOTES Source Code.  (C)2012 PhiloLabs

#include "OgreStableHeaders.h"

#include "OgreZip.h"

#include "core/logManager.h"
#include "core/exception.h"
#include "OgreStringVector.h"
#include "OgreRoot.h"

#include <zzip/zzip.h>
#include <zzip/plugin.h>


namespace Ogre {

    /// Utility method to format out zzip errors
    String getZzipErrorDescription(zzip_error_t zzipError) 
    {
        String errorMsg;
        switch (zzipError)
        {
        case ZZIP_NO_ERROR:
            break;
        case ZZIP_OUTOFMEM:
            errorMsg = "Out of memory.";
            break;            
        case ZZIP_DIR_OPEN:
        case ZZIP_DIR_STAT: 
        case ZZIP_DIR_SEEK:
        case ZZIP_DIR_READ:
            errorMsg = "Unable to read zip file.";
            break;            
        case ZZIP_UNSUPP_COMPR:
            errorMsg = "Unsupported compression format.";
            break;            
        case ZZIP_CORRUPTED:
            errorMsg = "Corrupted archive.";
            break;            
        default:
            errorMsg = "Unknown error.";
            break;            
        };

        return errorMsg;
    }
    //-----------------------------------------------------------------------
    ZipArchive::ZipArchive(const String& name, const String& archType, zzip_plugin_io_handlers* pluginIo)
        : Archive(name, archType), mZzipDir(0), mPluginIo(pluginIo)
    {
    }
    //-----------------------------------------------------------------------
    ZipArchive::~ZipArchive()
    {
        unload();
    }
    //-----------------------------------------------------------------------
    void ZipArchive::load()
    {
		OGRE_LOCK_AUTO_MUTEX
        if (!mZzipDir)
        {
            zzip_error_t zzipError;
            mZzipDir = zzip_dir_open_ext_io(mName.c_str(), &zzipError, 0, mPluginIo);
            checkZzipError(zzipError, "opening archive");

            // Cache names
            ZZIP_DIRENT zzipEntry;
            while (zzip_dir_read(mZzipDir, &zzipEntry))
            {
                FileInfo info;
				info.archive = this;
                // Get basename / path
                StringUtil::splitFilename(zzipEntry.d_name, info.basename, info.path);
                info.filename = zzipEntry.d_name;
                // Get sizes
                info.compressedSize = static_cast<size_t>(zzipEntry.d_csize);
                info.uncompressedSize = static_cast<size_t>(zzipEntry.st_size);
                // folder entries
                if (info.basename.empty())
                {
                    info.filename = info.filename.substr (0, info.filename.length () - 1);
                    StringUtil::splitFilename(info.filename, info.basename, info.path);
                    // Set compressed size to -1 for folders; anyway nobody will check
                    // the compressed size of a folder, and if he does, its useless anyway
                    info.compressedSize = size_t (-1);
                }
                else
                {
                    info.filename = info.basename;
                }

                mFileList.push_back(info);

            }

        }
    }
    //-----------------------------------------------------------------------
    void ZipArchive::unload()
    {
		OGRE_LOCK_AUTO_MUTEX
        if (mZzipDir)
        {
            zzip_dir_close(mZzipDir);
            mZzipDir = 0;
            mFileList.clear();
        }
    
    }
    //-----------------------------------------------------------------------
	DataStreamPtr ZipArchive::open(const String& filename, bool readOnly) const
    {
		// zziplib is not threadsafe
		OGRE_LOCK_AUTO_MUTEX
        String lookUpFileName = filename;

        // Format not used here (always binary)
        ZZIP_FILE* zzipFile = 
            zzip_file_open(mZzipDir, lookUpFileName.c_str(), ZZIP_ONLYZIP | ZZIP_CASELESS);
        if (!zzipFile) // Try if we find the file
        {
            const Ogre::FileInfoListPtr fileNfo = findFileInfo(lookUpFileName, true);
            if (fileNfo->size() == 1) // If there are more files with the same do not open anyone
            {
                Ogre::FileInfo info = fileNfo->at(0);
                lookUpFileName = info.path + info.basename;
                zzipFile = zzip_file_open(mZzipDir, lookUpFileName.c_str(), ZZIP_ONLYZIP | ZZIP_CASELESS); // When an error happens here we will catch it below
            }
        }

        if (!zzipFile)
		{
            int zerr = zzip_error(mZzipDir);
            String zzDesc = getZzipErrorDescription((zzip_error_t)zerr);

			Core::LogManager::Instance()->stream().write(
				"%s - Unable to open file %s, error was '%s'",mName.c_str(), lookUpFileName.c_str(), zzDesc.c_str() );
                
			// return null pointer
			return DataStreamPtr();
		}

		// Get uncompressed size too
		ZZIP_STAT zstat;
		zzip_dir_stat(mZzipDir, lookUpFileName.c_str(), &zstat, ZZIP_CASEINSENSITIVE);

        // Construct & return stream
        return DataStreamPtr(PHILO_NEW ZipDataStream(lookUpFileName, zzipFile, static_cast<size_t>(zstat.st_size)));

    }
	//---------------------------------------------------------------------
	DataStreamPtr ZipArchive::create(const String& filename) const
	{
		PHILO_EXCEPT(Exception::ERR_NOT_IMPLEMENTED, 
			"Modification of zipped archives is not supported", 
			"ZipArchive::create");

	}
	//---------------------------------------------------------------------
	void ZipArchive::remove(const String& filename) const
	{
	}
    //-----------------------------------------------------------------------
    StringVectorPtr ZipArchive::list(bool recursive, bool dirs)
    {
		OGRE_LOCK_AUTO_MUTEX
        StringVectorPtr ret = StringVectorPtr(PH_NEW_T(StringVector, Memory::ObjectHeap)(), SPFM_DELETE_T);

        FileInfoList::iterator i, iend;
        iend = mFileList.end();
        for (i = mFileList.begin(); i != iend; ++i)
            if ((dirs == (i->compressedSize == size_t (-1))) &&
                (recursive || i->path.empty()))
                ret->push_back(i->filename);

        return ret;
    }
    //-----------------------------------------------------------------------
    FileInfoListPtr ZipArchive::listFileInfo(bool recursive, bool dirs)
    {
		OGRE_LOCK_AUTO_MUTEX
        FileInfoList* fil = PH_NEW_T(FileInfoList, Memory::ObjectHeap)();
        FileInfoList::const_iterator i, iend;
        iend = mFileList.end();
        for (i = mFileList.begin(); i != iend; ++i)
            if ((dirs == (i->compressedSize == size_t (-1))) &&
                (recursive || i->path.empty()))
                fil->push_back(*i);

        return FileInfoListPtr(fil, SPFM_DELETE_T);
    }
    //-----------------------------------------------------------------------
    StringVectorPtr ZipArchive::find(const String& pattern, bool recursive, bool dirs)
    {
		OGRE_LOCK_AUTO_MUTEX
        StringVectorPtr ret = StringVectorPtr(PH_NEW_T(StringVector, Memory::ObjectHeap)(), SPFM_DELETE_T);
        // If pattern contains a directory name, do a full match
        bool full_match = (pattern.find ('/') != String::npos) ||
                          (pattern.find ('\\') != String::npos);

        FileInfoList::iterator i, iend;
        iend = mFileList.end();
        for (i = mFileList.begin(); i != iend; ++i)
            if ((dirs == (i->compressedSize == size_t (-1))) &&
                (recursive || full_match || i->path.empty()))
                // Check basename matches pattern (zip is case insensitive)
                if (StringUtil::match(full_match ? i->filename : i->basename, pattern, false))
                    ret->push_back(i->filename);

        return ret;
    }
    //-----------------------------------------------------------------------
	FileInfoListPtr ZipArchive::findFileInfo(const String& pattern, 
        bool recursive, bool dirs) const
    {
		OGRE_LOCK_AUTO_MUTEX
        FileInfoListPtr ret = FileInfoListPtr(PH_NEW_T(FileInfoList, Memory::ObjectHeap)(), SPFM_DELETE_T);
        // If pattern contains a directory name, do a full match
        bool full_match = (pattern.find ('/') != String::npos) ||
                          (pattern.find ('\\') != String::npos);

        FileInfoList::const_iterator i, iend;
        iend = mFileList.end();
        for (i = mFileList.begin(); i != iend; ++i)
            if ((dirs == (i->compressedSize == size_t (-1))) &&
                (recursive || full_match || i->path.empty()))
                // Check name matches pattern (zip is case insensitive)
                if (StringUtil::match(full_match ? i->filename : i->basename, pattern, false))
                    ret->push_back(*i);

        return ret;
    }
    //-----------------------------------------------------------------------
	bool ZipArchive::exists(const String& filename)
	{
		// zziplib is not threadsafe
		OGRE_LOCK_AUTO_MUTEX
		ZZIP_STAT zstat;
		int res = zzip_dir_stat(mZzipDir, filename.c_str(), &zstat, ZZIP_CASEINSENSITIVE);

		return (res == ZZIP_NO_ERROR);

	}
	//---------------------------------------------------------------------
	time_t ZipArchive::getModifiedTime(const String& filename)
	{
		// Zziplib doesn't yet support getting the modification time of individual files
		// so just check the mod time of the zip itself
		struct stat tagStat;
		bool ret = (stat(mName.c_str(), &tagStat) == 0);

		if (ret)
		{
			return tagStat.st_mtime;
		}
		else
		{
			return 0;
		}

	}
	//-----------------------------------------------------------------------
    void ZipArchive::checkZzipError(int zzipError, const String& operation) const
    {
        if (zzipError != ZZIP_NO_ERROR)
        {
            String errorMsg = getZzipErrorDescription(static_cast<zzip_error_t>(zzipError));

            PHILO_EXCEPT(Exception::ERR_INTERNAL_ERROR, 
                mName + " - error whilst " + operation + ": " + errorMsg,
                "ZipArchive::checkZzipError");
        }
    }
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    ZipDataStream::ZipDataStream(ZZIP_FILE* zzipFile, size_t uncompressedSize)
        : mZzipFile(zzipFile)
    {
		mSize = uncompressedSize;
    }
    //-----------------------------------------------------------------------
    ZipDataStream::ZipDataStream(const String& name, ZZIP_FILE* zzipFile, size_t uncompressedSize)
        :DataStream(name), mZzipFile(zzipFile)
    {
		mSize = uncompressedSize;
    }
    //-----------------------------------------------------------------------
	ZipDataStream::~ZipDataStream()
	{
		close();
	}
    //-----------------------------------------------------------------------
    size_t ZipDataStream::read(void* buf, size_t count)
    {
		size_t was_avail = mCache.read(buf, count);
		zzip_ssize_t r = 0;
		if (was_avail < count)
		{
			r = zzip_file_read(mZzipFile, (char*)buf + was_avail, count - was_avail);
			if (r<0) {
				ZZIP_DIR *dir = zzip_dirhandle(mZzipFile);
				String msg = zzip_strerror_of(dir);
				PHILO_EXCEPT(Exception::ERR_INTERNAL_ERROR,
					mName+" - error from zziplib: "+msg,
					"ZipDataStream::read");
			}
			mCache.cacheData((char*)buf + was_avail, (size_t)r);
		}
		return was_avail + (size_t)r;
    }
	//---------------------------------------------------------------------
	size_t ZipDataStream::write(void* buf, size_t count)
	{
		// not supported
		return 0;
	}
    //-----------------------------------------------------------------------
    void ZipDataStream::skip(long count)
    {
        long was_avail = static_cast<long>(mCache.avail());
		if (count > 0)
		{
			if (!mCache.ff(count))
				zzip_seek(mZzipFile, static_cast<zzip_off_t>(count - was_avail), SEEK_CUR);
		}
		else if (count < 0)
		{
			if (!mCache.rewind((size_t)(-count)))
				zzip_seek(mZzipFile, static_cast<zzip_off_t>(count + was_avail), SEEK_CUR);
		}
    }
    //-----------------------------------------------------------------------
    void ZipDataStream::seek( size_t pos )
    {
		zzip_off_t newPos = static_cast<zzip_off_t>(pos);
		zzip_off_t prevPos = static_cast<zzip_off_t>(tell());
		if (prevPos < 0)
		{
			// seek set after invalid pos
			mCache.clear();
			zzip_seek(mZzipFile, newPos, SEEK_SET);
		}
		else
		{
			// everything is going all right, relative seek
			skip(newPos - prevPos);
		}
    }
    //-----------------------------------------------------------------------
    size_t ZipDataStream::tell(void) const
    {
		zzip_off_t pos = zzip_tell(mZzipFile);
		if (pos<0)
			return (size_t)(-1);
		return static_cast<size_t>(pos) - mCache.avail();
    }
    //-----------------------------------------------------------------------
    bool ZipDataStream::eof(void) const
    {
        return (tell() >= mSize);
    }
    //-----------------------------------------------------------------------
    void ZipDataStream::close(void)
    {
		if (mZzipFile != 0)
		{
			zzip_file_close(mZzipFile);
			mZzipFile = 0;
		}
		mCache.clear();
    }
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //  ZipArchiveFactory
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    const String& ZipArchiveFactory::getType(void) const
    {
        static String name = "Zip";
        return name;
    }
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //  EmbeddedZipArchiveFactory
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    /// a struct to hold embedded file data
    struct EmbeddedFileData
    {
        const uint8 * fileData;
        zzip_size_t fileSize;
        zzip_size_t curPos;
        bool isFileOpened;
        EmbeddedZipArchiveFactory::DecryptEmbeddedZipFileFunc decryptFunc;
    };
    //-----------------------------------------------------------------------
    /// A type for a map between the file names to file index
    typedef map<String, int>::type FileNameToIndexMap;
    typedef FileNameToIndexMap::iterator FileNameToIndexMapIter;
    /// A type to store the embedded files data
    typedef vector<EmbeddedFileData>::type EmbbedFileDataList;
    /// A static map between the file names to file index
    FileNameToIndexMap * EmbeddedZipArchiveFactory_mFileNameToIndexMap;
    /// A static list to store the embedded files data
    EmbbedFileDataList * EmbeddedZipArchiveFactory_mEmbbedFileDataList;
    /// A static pointer to file io alternative implementation for the embedded files
    zzip_plugin_io_handlers* EmbeddedZipArchiveFactory::mPluginIo = NULL;
    _zzip_plugin_io sEmbeddedZipArchiveFactory_PluginIo;
    #define EMBED_IO_BAD_FILE_HANDLE (-1)
    #define EMBED_IO_SUCCESS (0)
    //-----------------------------------------------------------------------
    /// functions for embedded zzip_plugin_io_handlers implementation 
    /// The functions are here and not as static members because they 
    /// use types that I don't want to define in the header like zzip_char_t,
    //  zzip_ssize_t and such.
    //-----------------------------------------------------------------------
    // get file date by index
    EmbeddedFileData & getEmbeddedFileDataByIndex(int fd)
    {
        return (*EmbeddedZipArchiveFactory_mEmbbedFileDataList)[fd-1];
    }
    //-----------------------------------------------------------------------
    // opens the file
    int EmbeddedZipArchiveFactory_open(zzip_char_t* name, int flags, ...)
    {
        String nameAsString = name;
        FileNameToIndexMapIter foundIter = EmbeddedZipArchiveFactory_mFileNameToIndexMap->find(nameAsString);
        if (foundIter != EmbeddedZipArchiveFactory_mFileNameToIndexMap->end())
        {
            int fd = foundIter->second;
            EmbeddedFileData & curEmbeddedFileData = getEmbeddedFileDataByIndex(fd);
            if(curEmbeddedFileData.isFileOpened)
            {
               // file is opened - return an error handle
               return EMBED_IO_BAD_FILE_HANDLE;
            }
            
            curEmbeddedFileData.isFileOpened = true;
            return fd;
        }
        else
        {
           // not found - return an error handle
           return EMBED_IO_BAD_FILE_HANDLE;
        }
    }
    //-----------------------------------------------------------------------
    // Closes a file.
    // Return Value - On success, close returns 0. 
    int EmbeddedZipArchiveFactory_close(int fd)
    {
        if (fd == EMBED_IO_BAD_FILE_HANDLE)
        {
            // bad index - return an error
            return -1;
        }

        EmbeddedFileData & curEmbeddedFileData = getEmbeddedFileDataByIndex(fd);

        if(curEmbeddedFileData.isFileOpened == false)
        {
           // file is opened - return an error handle
           return -1;
        }
        else
        {
            // success
            curEmbeddedFileData.isFileOpened = false;
            curEmbeddedFileData.curPos = 0;
            return 0;
        }

    }
       
    //-----------------------------------------------------------------------
    // reads data from the file
    zzip_ssize_t EmbeddedZipArchiveFactory_read(int fd, void* buf, zzip_size_t len)
    {
        if (fd == EMBED_IO_BAD_FILE_HANDLE)
        {
            // bad index - return an error size - negative
            return -1;
        }
        // get the current buffer in file;
        EmbeddedFileData & curEmbeddedFileData = getEmbeddedFileDataByIndex(fd);
        const uint8 * curFileData = curEmbeddedFileData.fileData;
        if (len + curEmbeddedFileData.curPos > curEmbeddedFileData.fileSize)
        {
            len = curEmbeddedFileData.fileSize - curEmbeddedFileData.curPos;
        }
        curFileData += curEmbeddedFileData.curPos;
        
        // copy to out buffer
        memcpy(buf, curFileData, len);

        if( curEmbeddedFileData.decryptFunc != NULL )
        {
            if (!curEmbeddedFileData.decryptFunc(curEmbeddedFileData.curPos, buf, len))
            {
                // decrypt failed - return an error size - negative
                return -1;
            }
        }

        // move the cursor to the new pos
        curEmbeddedFileData.curPos += len;
        
        return len;
    }
    //-----------------------------------------------------------------------
    // Moves file pointer.
    zzip_off_t EmbeddedZipArchiveFactory_seeks(int fd, zzip_off_t offset, int whence)
    {
        if (fd == EMBED_IO_BAD_FILE_HANDLE)
        {
            // bad index - return an error - nonzero value.
            return -1;
        }
        
        zzip_size_t newPos = -1;
        // get the current buffer in file;
        EmbeddedFileData & curEmbeddedFileData = getEmbeddedFileDataByIndex(fd);
        switch(whence)
        {
            case SEEK_CUR:
                newPos = curEmbeddedFileData.curPos + offset;
                break;
            case SEEK_END:
                newPos = curEmbeddedFileData.fileSize - offset;
                break;
            case SEEK_SET:
                newPos = offset;
                break;
            default:
                // bad whence - return an error - nonzero value.
                return -1;
                break;
        };
        if (newPos >= curEmbeddedFileData.fileSize || 
            newPos < 0 )
        {
            // bad whence - return an error - nonzero value.
            return -1;
        }

        curEmbeddedFileData.curPos = newPos;
        return newPos;
    }
    //-----------------------------------------------------------------------
    // returns the file size
    zzip_off_t EmbeddedZipArchiveFactory_filesize(int fd)
    {
        if (fd == EMBED_IO_BAD_FILE_HANDLE)
        {
            // bad index - return an error - nonzero value.
            return -1;
        }
                // get the current buffer in file;
        EmbeddedFileData & curEmbeddedFileData = getEmbeddedFileDataByIndex(fd);
        return curEmbeddedFileData.fileSize;
    }
    //-----------------------------------------------------------------------
    // writes data to the file
    zzip_ssize_t EmbeddedZipArchiveFactory_write(int fd, _zzip_const void* buf, zzip_size_t len)
    {
        // the files in this case are read only - return an error  - nonzero value.
        return -1;
    }
    //-----------------------------------------------------------------------
    EmbeddedZipArchiveFactory::EmbeddedZipArchiveFactory()
    {
        // init static member
        if (mPluginIo == NULL)
        {
            mPluginIo = &sEmbeddedZipArchiveFactory_PluginIo;
            mPluginIo->fd.open = EmbeddedZipArchiveFactory_open;    
            mPluginIo->fd.close = EmbeddedZipArchiveFactory_close;    
            mPluginIo->fd.read = EmbeddedZipArchiveFactory_read;    
            mPluginIo->fd.seeks = EmbeddedZipArchiveFactory_seeks;    
            mPluginIo->fd.filesize = EmbeddedZipArchiveFactory_filesize;    
            mPluginIo->fd.write = EmbeddedZipArchiveFactory_write;    
            mPluginIo->fd.sys = 1;    
            mPluginIo->fd.type = 1;    
        }
    }
    //-----------------------------------------------------------------------
    EmbeddedZipArchiveFactory::~EmbeddedZipArchiveFactory()
    {
    }    
    //-----------------------------------------------------------------------
    const String& EmbeddedZipArchiveFactory::getType(void) const
    {
        static String name = "EmbeddedZip";
        return name;
    }
    //-----------------------------------------------------------------------
    void EmbeddedZipArchiveFactory::addEmbbeddedFile(const String& name, const uint8 * fileData, 
                                        size_t fileSize, DecryptEmbeddedZipFileFunc decryptFunc)
    {
        static bool needToInit = true;
        if(needToInit)
        {
            needToInit = false;

            // we can't be sure when global variables get initialized
            // meaning it is possible our list has not been init when this
            // function is being called. The solution is to use local
            // static members in this function an init the pointers for the
            // global here. We know for use that the local static variables
            // are create in this stage.
            static FileNameToIndexMap sFileNameToIndexMap;
            static EmbbedFileDataList sEmbbedFileDataList;
            EmbeddedZipArchiveFactory_mFileNameToIndexMap = &sFileNameToIndexMap;
            EmbeddedZipArchiveFactory_mEmbbedFileDataList = &sEmbbedFileDataList;
        }

        EmbeddedFileData newEmbeddedFileData;
        newEmbeddedFileData.curPos = 0;
        newEmbeddedFileData.isFileOpened = false;
        newEmbeddedFileData.fileData = fileData;
        newEmbeddedFileData.fileSize = fileSize;
        newEmbeddedFileData.decryptFunc = decryptFunc;
        EmbeddedZipArchiveFactory_mEmbbedFileDataList->push_back(newEmbeddedFileData);
        (*EmbeddedZipArchiveFactory_mFileNameToIndexMap)[name] = EmbeddedZipArchiveFactory_mEmbbedFileDataList->size();
    }
    //-----------------------------------------------------------------------
    void EmbeddedZipArchiveFactory::removeEmbbeddedFile( const String& name )
    {
        EmbeddedZipArchiveFactory_mFileNameToIndexMap->erase(name);
    }
}
